fix(client): skip the SSE fallback when the server/discover probe is rejected with 400/404 - #1855
Open
scottt732 wants to merge 1 commit into
Open
Conversation
…rejected A server predating SEP-2575 rejects the session-less server/discover POST at the HTTP layer with 400 (cannot parse the request) or 404 (requires Mcp-Session-Id on every non-initialize POST). AutoDetectingClientSessionTransport treated that like any other non-JSON-RPC error and attempted the SSE fallback, but those two statuses say nothing about which transport the peer speaks -- an SSE-only server rejects the probe exactly the same way. McpClientImpl.ConnectAsync already reads 400/404 from the probe as 'this server requires the initialize handshake' and retries with initialize on the same transport, and that retry still falls back to SSE. So the SSE GET spent during the probe is always discarded: a Streamable-HTTP-only server that predates SEP-2575 pays a wasted round trip on every connect and logs 'falling back to SSE transport' for settled protocol negotiation, while an SSE-only server is reached one POST later either way. Scoped to the discover probe and to the two statuses ConnectAsync acts on, so every other failure keeps the existing fallback.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
AutoDetectingClientSessionTransportattempts the SSE fallback when the SEP-2575server/discoverprobe is rejected with 400 or 404. Neither status says anything about which transport the peer speaks: a server predating SEP-2575 rejects the session-lessserver/discoverPOST the same way whether it is Streamable-HTTP-only or SSE-only (400 when it cannot parse the request, 404 when it requiresMcp-Session-Idon every non-initializePOST).McpClientImpl.ConnectAsyncalready reads exactly these two statuses as "this server requires the initialize handshake" and retries withinitializeon the same transport — and that attempt still falls back to SSE. So the SSE GET spent during the probe is always discarded:streamable HTTP transport failed with status code NotFound, falling back to SSE transportat Information for what is settled protocol negotiation rather than a failure;This skips the fallback for that one case, scoped to the
server/discoverprobe and to the two statusesConnectAsyncacts on. Every other failure keeps the existing behavior.Observable behavior this addresses
Datadog's managed MCP server (
datadog-mcp 1.0.0,https://mcp.us5.datadoghq.com/api/unstable/mcp-server/mcp) is a Streamable-HTTP-only server predating SEP-2575:POST server/discover(no session)GETPOST initializeEvery connect currently costs discover-POST -> SSE-GET (405) -> initialize-POST, and logs the misleading fallback line. With this change it is discover-POST -> initialize-POST.
Relationship to existing work
ConnectAsyncdiscover→initialize fallback unreachable whenAutoDetecttransport fails withHttpRequestException#1848. That issue's "Fix 1" — catchingHttpRequestException400/404 inConnectAsync— is already inmain, which is what makes theinitializeretry reachable and therefore makes the SSE attempt redundant here. This PR does not touch theTryReadJsonRpcErrorAsynccontent-type gap that SDK Client:ConnectAsyncdiscover→initialize fallback unreachable whenAutoDetecttransport fails withHttpRequestException#1848 also raises.Verification
--filter 'FullyQualifiedName~HttpClientTransportAutoDetectTests': 13 passed on both net9.0 and net10.0 (9 pre-existing + 4 new). net8.0 could not run locally — no .NET 8 runtime on this machine.Full
ModelContextProtocol.Testson net10.0: 2361 passed, 2 failed — bothDockerEverythingServerTests, which fail identically on unmodifiedmainin this environment (container startup, not transport).ModelContextProtocol.AspNetCore.Testson net10.0: 615 passed, 0 failed.DockerEverythingServerTests.ConnectAndReceiveMessage_EverythingServerWithSse— a real SSE-only server reached over AutoDetect — passes with the change, and the second new test pins that same path at the transport level.New tests:
AutoDetectMode_SkipsSseFallback_WhenDiscoverProbeIsRejected(Theory: 404, 400) — no GET is issued, the original status surfaces, and no fallback line is logged.AutoDetectMode_StillFallsBackToSse_WhenInitializeFollowsRejectedDiscoverProbe— an SSE-only server is still reached, via theinitializeretry.AutoDetectMode_FallsBackToSse_WhenDiscoverProbeFailsWithUnrelatedStatus(415) — the skip stays scoped to 400/404.